package net.kucoe.grin.context;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.logging.Logger;

import net.kucoe.grin.*;
import net.kucoe.grin.System;
import net.kucoe.grin.context.application.*;
import net.kucoe.grin.context.exception.MarkMissingException;
import net.kucoe.grin.internal.ConstructorSteppingCreator;
import net.kucoe.grin.internal.InputInitialActor;

/**
 * Creates and wires context.
 * 
 * @author Vitaliy Basyuk
 */
public class ContextInitializer {
    
    private static final Logger logger = Grin.getLogger("ContextInitializer");
    
    /**
     * Initializes {@link Context} based on {@link GrinApplication} configuration.
     * 
     * @param application {@link GrinApplication}
     * @return {@link Context}
     */
    public Context initialize(final GrinApplication application) {
        Context context = new Context();
        List<InitSystem> initSystems = application.getInitSystems();
        List<ContextSystem> systems = new LinkedList<ContextSystem>();
        for (InitSystem initSystem : initSystems) {
            ContextSystem system = initializeSystem(initSystem);
            InitResult initResult = initSystem.getInitResult();
            system.setInitialResult(initResult.getClazz());
            system.setArguments(initResult.getInitArguments());
            systems.add(system);
        }
        context.setSystems(systems);
        return context;
    }
    
    private ContextSystem initializeSystem(final InitSystem initSystem) {
        Class<?> clazz = initSystem.getClazz();
        logger.config("Loading system from class " + clazz);
        checkMark(clazz, System.class);
        Object wiredObject = ConstructorSteppingCreator.create(clazz, (Object[]) null);
        Map<String, ContextActor> actors = new HashMap<String, ContextActor>();
        Map<String, ContextSpace> spaces = new HashMap<String, ContextSpace>();
        Map<String, ContextResult> results = new HashMap<String, ContextResult>();
        Map<String, ContextStrategy> strategies = new HashMap<String, ContextStrategy>();
        for (Field field : getAllFields(clazz)) {
            Class<?> type = field.getType();
            if (field.isAnnotationPresent(Actor.class)) {
                ContextActor actor = initializeActor(type);
                actors.put(type.getName(), actor);
                setAndCatch(field, wiredObject, actor);
                for (ContextStrategy strategy : actor.getStrategies().values()) {
                    Class<?> returnType = strategy.getMethod().getReturnType();
                    results.put(returnType.getName(), new ContextResult(returnType));
                }
            }
            if (field.isAnnotationPresent(Space.class)) {
                ContextSpace space = initializeSpace(type);
                spaces.put(type.getName(), space);
                setAndCatch(field, wiredObject, space);
            }
        }
        List<Method> allMethods = getAllMethods(clazz);
        for (Method method : allMethods) {
            if (method.isAnnotationPresent(Strategy.class)) {
                String name = method.getName();
                Class<?> resultClass = method.getReturnType();
                ContextStrategy strategy = new ContextStrategy(method, wiredObject);
                strategies.put(name, strategy);
                results.put(resultClass.getName(), new ContextResult(resultClass));
                Strategy annotation = method.getAnnotation(Strategy.class);
                Class<?>[] applicableActors = annotation.value();
                for (Class<?> actorClazz : applicableActors) {
                    ContextActor contextActor = actors.get(actorClazz.getName());
                    if (contextActor != null) {
                        String fullName = clazz.getName() + "#" + name;
                        contextActor.applyStrategy(fullName, strategy);
                    }
                }
            }
        }
        for (Method method : allMethods) {
            if (method.isAnnotationPresent(Validator.class)) {
                String name = method.getName();
                ContextValidator validator = new ContextValidator(method, wiredObject);
                Validator annotation = method.getAnnotation(Validator.class);
                Class<?>[] applicableResults = annotation.value();
                if (applicableResults.length == 0) {
                    for (ContextResult contextResult : results.values()) {
                        String fullName = clazz.getName() + "#" + name;
                        contextResult.applyValidator(fullName, validator);
                    }
                }
                for (Class<?> resultClazz : applicableResults) {
                    ContextResult contextResult = results.get(resultClazz.getName());
                    if (contextResult != null) {
                        String fullName = clazz.getName() + "#" + name;
                        contextResult.applyValidator(fullName, validator);
                    }
                }
            }
        }
        ContextActor inputActor = initializeActor(InputInitialActor.class);
        ContextSystem system = new ContextSystem(wiredObject, actors, spaces, results, strategies, inputActor);
        return system;
    }
    
    protected ContextActor initializeActor(final Class<?> clazz) {
        logger.config("Loading actor from class " + clazz);
        Object wiredObject = ConstructorSteppingCreator.create(clazz, (Object[]) null);
        Map<String, ContextStrategy> strategies = new HashMap<String, ContextStrategy>();
        Map<String, ContextParameter> parameters = new HashMap<String, ContextParameter>();
        ContextStrategy parametersLookupStrategy = null;
        for (Method method : getAllMethods(clazz)) {
            if (method.isAnnotationPresent(Strategy.class)) {
                ContextStrategy strategy = new ContextStrategy(method, wiredObject);
                strategies.put(method.getName(), strategy);
            }
            if (method.isAnnotationPresent(Parameter.class)) {
                parametersLookupStrategy = new ContextStrategy(method, wiredObject);
            }
        }
        for (Field field : getAllFields(clazz)) {
            String name = field.getName();
            if (field.isAnnotationPresent(Parameter.class)) {
                ContextParameter parameter = new ContextParameter(field);
                parameters.put(name, parameter);
            }
        }
        ContextActor actor = new ContextActor(wiredObject, strategies, parameters, parametersLookupStrategy);
        return actor;
    }
    
    protected ContextSpace initializeSpace(final Class<?> clazz) {
        logger.config("Loading space from class " + clazz);
        Object wiredObject = ConstructorSteppingCreator.create(clazz, (Object[]) null);
        ContextSpace space = new ContextSpace(wiredObject);
        return space;
    }
    
    protected ContextItem initializeItem(final Class<?> clazz) {
        logger.config("Loading item from class " + clazz);
        Object wiredObject = ConstructorSteppingCreator.create(clazz, (Object[]) null);
        ContextItem item = new ContextItem(wiredObject);
        return item;
    }
    
    private void setAndCatch(final Field field, final Object object, final ContextWired contextObject) {
        Object value = contextObject.getWired();
        if (object != null) {
            try {
                field.set(object, value);
            } catch (IllegalArgumentException e) {
                // skip
            } catch (IllegalAccessException e) {
                // skip
            }
        }
    }
    
    private List<Field> getAllFields(final Class<?> clazz) {
        List<Field> fields = new ArrayList<Field>();
        Class<?> parent = clazz.getSuperclass();
        if (parent != null) {
            fields.addAll(getAllFields(parent));
        }
        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);
            fields.add(field);
        }
        return fields;
    }
    
    private List<Method> getAllMethods(final Class<?> clazz) {
        List<Method> methods = new ArrayList<Method>();
        Class<?> parent = clazz.getSuperclass();
        if (parent != null) {
            methods.addAll(getAllMethods(parent));
        }
        for (Method method : clazz.getDeclaredMethods()) {
            method.setAccessible(true);
            methods.add(method);
        }
        return methods;
    }
    
    private void checkMark(final Class<?> clazz, final Class<? extends Annotation> mark) {
        if (!clazz.isAnnotationPresent(mark)) {
            throw new MarkMissingException("Class " + clazz.getName() + " was ment to be annotated with " + mark
                    + ", but isn't. Maybe you forgot to add annotation?");
        }
    }
}
